[ZIO] ZLayerを使ったモジュール分割と依存性注入
はじめに
ZIO のドキュメント How to Use Modules and Layers? を 参考にZLayerを使って簡単なプログラム(FizzBuzz)を作ってみました。
ZLayerとは
ZLayerの定義は以下の通りです。ZLayerはZIOで構成されるプログラムのレイヤーを表します。レイヤーは何らかのService(RIn
)を入力として別のサービス(ROut
)を出力します。
レイヤーの生成は副作用をもたらす場合があります(e.g. ファイルディスクリプタのオープンやDBコネクション)。生成中のエラーはE
で表されます。また生成されたリソースは安全に破棄される必要があります(ZManagedからレイヤーを生成できる)。
sealed abstract class ZLayer[-RIn, +E, +ROut] {}
ServiceとLayerの例
FizzzBuzzの入力値を生成するサービス FizzBuzzSeedGenとそれを提供するレイヤーの例を示します。
コンポーネントのインタフェースをobject内にServiceとして定義して、パッケージオブジェクトにサービスへの依存性をHas[A]
でエイリアス定義するのが推奨されているパターンです。
サービスを提供するレイヤーとして具象クラスを定義しています。このレイヤーでは乱数の生成元としてzio.random.Random
を入力としてFizzBuzzSeedGen.Serivceを出力します。
package examples import zio.macros.accessible import zio.random.Random import zio.{Has, UIO, ZLayer} package object layer_example { final case class FizzBuzzInput(num: Long) sealed trait FizzBuzzAnswer object FizzBuzzAnswer { final case class Just(num: Long) extends FizzBuzzAnswer final case class Fizz(num: Long) extends FizzBuzzAnswer final case class Buzz(num: Long) extends FizzBuzzAnswer final case class FizzBuzz(num: Long) extends FizzBuzzAnswer } type FizzBuzzSeedGen = Has[FizzBuzzSeedGen.Service] @accessible object FizzBuzzSeedGen { val live: ZLayer[Random, Nothing, FizzBuzzSeedGen] = ZLayer.fromFunction { rng => new Service { override def randomOne: UIO[FizzBuzzInput] = rng.get.nextLongBetween(0, 100).map(FizzBuzzInput) } } trait Service { def randomOne: UIO[FizzBuzzInput] } } }
レイヤーの合成と環境としての使用例
次のこのレイヤーの使用例を示します。#run
の中でレイヤーを合成してprintFizzBuzzおよびfizzbuzzが必要とするサービスを提供するレイヤーを生成します。ZIOへの環境の注入は#providerLayer
または#provideCustomLayer
で行えます。providerCustomLayerはzio.ZEnv以外に依存するものがないレイヤーを生成します。
依存性の注入は >>>
で行います。以下の例ではrandom
を partialClientLayer
に注入してZLayer[Any, Nothing, FizzBuzzSeedGen]
を得ています。ZLayerには他にもいくつかのオペレーターが定義されていて様々なレイヤーの合成が行えます。
package examples.layer_example import zio.{ExitCode, URIO, ZIO, ZLayer} object FizzBuzzGame extends zio.App { import FizzBuzzAnswer._ override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = { val partialGen: ZLayer[zio.random.Random, Nothing, FizzBuzzSeedGen] = FizzBuzzSeedGen.live val random: ZLayer[Any, Nothing, zio.random.Random] = zio.random.Random.live val client: ZLayer[Any, Nothing, FizzBuzzSeedGen] = random >>> partialGen for { _ <- printFizzBuzz.provideCustomLayer(client) } yield ExitCode.success } def printFizzBuzz: ZIO[FizzBuzzSeedGen with zio.console.Console, Nothing, Unit] = for { ans <- fizzbuzz _ <- ZIO.accessM[zio.console.Console](_.get.putStrLn(ans.toString)) } yield () def fizzbuzz: ZIO[FizzBuzzSeedGen, Nothing, FizzBuzzAnswer] = for { r <- ZIO.accessM[FizzBuzzSeedGen](_.get.randomOne) ans = r.num match { case _ if r.num % 3 == 0 && r.num % 5 == 0 => FizzBuzz(r.num) case _ if r.num % 3 == 0 => Fizz(r.num) case _ if r.num % 5 == 0 => Buzz(r.num) case _ => Just(r.num) } } yield ans }
具象クラスの差し替え
ここまでみてきたようにあるサービスのインスタンスは依存するサービスを提供するレイヤーの合成によって得られます。レイヤーは任意に生成できるので、ユニットテスト時に依存するサービスをモックに差し替えることもできます。以下の例では乱数ではなくて固定の値を生成するFizzBuzzSeedGen
を与えてfizzbuzzメソッドをテストしています。
package examples.layer_example import munit.FunSuite import zio.{UIO, ZLayer} class FizzBuzzGameTest extends FunSuite { import FizzBuzzAnswer._ def fizzbuzz(num: Long): FizzBuzzAnswer = zio.Runtime.default.unsafeRun( FizzBuzzGame.fizzbuzz.provideLayer( ZLayer.succeed( new FizzBuzzSeedGen.Service { override def randomOne: UIO[FizzBuzzInput] = UIO(FizzBuzzInput(num)) }) )) test("fizzbuzz") { assertEquals(fizzbuzz(1), Just(1)) assertEquals(fizzbuzz(2), Just(2)) assertEquals(fizzbuzz(3), Fizz(3)) assertEquals(fizzbuzz(4), Just(4)) assertEquals(fizzbuzz(5), Buzz(5)) assertEquals(fizzbuzz(15), FizzBuzz(15)) } }
まとめ
ZLayerを使ったモジュール分割とレイヤーの合成による依存性の注入をしてみました。tagless finalほどコンパイルエラーに悩まされずにエフェクトコンテナとロジックを分離できそう・・・な気がしました。